﻿using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections.Generic;
using System.Linq;

using BizElements.Core;
using BizElements.BusinessLayer;

namespace BizElements.Web
{
    /// <summary>Base class for web forms that act as application controller and contain a single user control for viewing and editing of business objects which implement <see cref="BizElements.BusinessLayer.IBusinessObject"/> interface.</summary>
    public abstract class BusinessObjectFormBase<T> : Page
        where T : class, IBusinessObject
    {
        #region Abstract child control properties (user control, context, script manager). Getters in derived classes must return references to control.

        /// <summary>UI component which displays and edits a business object.</summary>
        protected abstract IBusinessObjectUI<T> UserControl { get; }

        /// <summary>HTML hidden field that contains <see cref="WebFormContext"/> data.</summary>
        protected abstract HiddenField WebFormContextHiddenField { get; }

        #endregion

        #region Init, Load, PreRender.

        /// <summary>Initialize <see cref="System.Web.UI.Page.UICulture"/> to match culture defined in <see cref="UserSession.UiCulture"/>.</summary>
        protected override void InitializeCulture()
        {   
            this.UICulture = UserSession.UiCulture.Name;
        }

        /// <summary>Checks that current user has permissions to open the form, ensures that the page is not cached in browser, prepares/fetches data and initializes child components.</summary>
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            var scriptManager = ScriptManager.GetCurrent(this);
            if (scriptManager != null)
                scriptManager.AsyncPostBackError += new EventHandler<AsyncPostBackErrorEventArgs>(HandleAsyncPostBackError);

            var bizobjMeta = NewBizObject().GetDescriptor();
            SecurityHelper.DemandFormPermission(bizobjMeta.Permissions.UserInterfacePermission);
            SecurityHelper.DemandFormPermission(bizobjMeta.Permissions.ReadPermission);
            WebUtil.DontCacheCurrentPageOnBrowser();
            if (!IsPostBack)
            {
                CleanUp();
                PrepareData();
                ApplyFormContext();
            }

            CheckOutExistingObject();
            this.UserControl.BizObject = this.BizObject;
            if (this.BizObject.IsReadOnly)
                this.UserControl.ReadOnly = true;
        }

        /// <summary>Focuses default control on first load, i.e. if not in postback.</summary>
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);            
        }

        /// <summary>Focuses default control on first load, ie. if not in postback.</summary>
        protected virtual void FocusDefaultControlIfNotPostBack()
        {
            if (!IsPostBack)
            {
                this.UserControl.Focus();
            }
        }

        /// <summary>Sets form/page title.</summary>
        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);
            SetPageTitle();
            FocusDefaultControlIfNotPostBack();
        }

        /// <summary>Sets form/page title.</summary>
        protected virtual void SetPageTitle()
        {
            if (string.IsNullOrEmpty(this.Title) && (this.BizObject != null))
                this.Title = this.BizObject.GetFieldCaption("");
        }

        #endregion

        #region Session properties.

        /// <summary>Removes business object from the session state.</summary>
        protected virtual void CleanUp()
        {
            this.BizObject = default(T);
        }

        /// <summary>Gets or sets business object. Objects reference is mantained in applications' session state.</summary>
        /// <remarks>The key is unique for each page but it's always generated using the same algorithm.
        /// This means that you cannot use the same page in multiple frames/web parts at the same time.
        /// Override the default implementation if you need to use different session keys.
        /// </remarks>
        protected virtual T BizObject
        {
            get { return WebUtil.GetSessionObject<T>(GetType(), "__BizObject"); }
            set { WebUtil.SetSessionVariable(GetType(), "__BizObject", value); }
        }

        #endregion

        #region WebFormContext.

        /// <summary>This member is never accessed directly. FormContext getter must always be used as it ensures that the context is instantiated/deserialized.</summary>
        WebFormContext formContext;

        /// <summary>Gets current form context. Ensures that it is initialized/deserialized. Context may be passed via session state or query string.</summary>
        protected virtual WebFormContext FormContext
        {
            get
            {
                // WebFormContext is serialized to WebFormContextHiddenField hidden field.
                WebFormContext.EnsureFormContext(ref this.formContext, this.WebFormContextHiddenField);
                return this.formContext;
            }
        }

        /// <summary>Applies ReadOnly and ChildrenHidden options as defined in <see cref="FormContext"/>.</summary>
        protected virtual void ApplyFormContext()
        {
            if (this.FormContext.ReadOnly)
                this.BizObject.IsReadOnly = true;

            if (this.FormContext.ChildrenHidden)
                this.UserControl.ChildrenHidden = true;
        }

        #endregion

        #region Prepare data.

        /// <summary>Fetches entity (using <see cref="FetchBizObject"/> method) in form context or creates a new one and sets it to <see cref="BizObject"/> property bound to session state.
        /// Parses entity properties defined in <see cref="FormContext"/>.</summary>
        protected virtual void PrepareData()
        {
            var newBizObject = NewBizObject();
            this.BizObject = newBizObject;
            object[] defaultKeyValues = newBizObject.GetPrimaryKeyValue();
            // PK may be parsed in the next line, too.
            this.FormContext.ParseEntityProperties(this.BizObject, PropertySelection.AllProperties);

            IDbTable table = newBizObject.Table;
            if (HasSingleFieldNumericOrTextPk(table) && !string.IsNullOrEmpty(this.FormContext.EntityId))
            {
                // FormContext.EntityId is the prefered source for PK.
                object id = NumberParser.ParseValue(table.PrimaryKey[0].DataType, this.FormContext.EntityId, UserSession.Formatter);
                this.BizObject.SetField(table.PrimaryKey[0], id);                
            }

            object[] pkValues = this.BizObject.GetPrimaryKeyValue();
            bool differsFromDefaultValues = !ArrayUtil.ContainEqualValues(pkValues, defaultKeyValues);
            if (differsFromDefaultValues && (pkValues != null))
            {
                bool exists = FetchBizObject();
                if (!exists)
                    throw new ArgumentException("Object with the specified ID cannot be fetched. ID: " + ArrayUtil.Concat(pkValues, ";"));
            }
            else
            {
                // Ensure parents whose IDs were parse by form context. If no FKs were parse then nothing is fetched.
                this.BizObject.EnsureParents(table.ForeignKeys);
            }            
        }

        /// <summary>Fetches business object and all of its parents.</summary>
        /// <returns><b>true</b> if object is found in the data-source; otherwise <b>false</b></returns>
        protected virtual bool FetchBizObject()
        {
            return this.BizObject.Fetch(UserSession.Actor, DataAccessScope.Parents, true);
        }

        private static bool HasSingleFieldNumericOrTextPk(IDbTable table)
        {
            bool haSingleFieldNumericOrTextPk = (table.PrimaryKey.Count == 1) && IsNumericOrTextualField(table.PrimaryKey[0]);
            return haSingleFieldNumericOrTextPk;
        }

        private static bool IsNumericOrTextualField(IDbColumn field)
        {
            return TypeUtil.IsNumber(field.DataType) || TypeUtil.IsText(field.DataType);
        }

        /// <summary>Creates a new business object via reflection. It is recommended to override this method in derived classes and use factory to create a new object.
        /// Sets <b>-1</b> value to PK field, passes user's culture to object's message source and disables exceptions that occure if non-nullable properties are not set.</summary>
        protected virtual T NewBizObject()
        {
            T newBizobj = Activator.CreateInstance<T>();
            newBizobj.SetField(newBizobj.Table.PrimaryKey[0], (sbyte)-1);
            (newBizobj as ICaptionProvider).MessageSource.CurrentCulture = UserSession.UiCulture;
            EntityModelBase classicEntity = newBizobj as EntityModelBase;
            if (classicEntity != null)
                classicEntity.NullCheckEnabled = false;

            return newBizobj;
        }

        #endregion

        #region Pessimistic offline concurrency control.

        /// <summary>Manages locks if pessimistic offline concurrency control is used. 
        /// Generates exception if exclusive lock is encountered. 
        /// Enters readonly mode if shared lock is encountered.
        /// Called in OnInit method before UI edit/readonly mode is set.</summary>
        protected virtual void CheckOutExistingObject()
        {
            LockMode? lockMode = this.BizObject.GetDescriptor().PessimisticLockMode;
            if (lockMode.HasValue && !this.BizObject.IsNew)
            {
                // Acquire lock only if in edit mode.
                if (this.BizObject.IsReadOnly)
                    EnsureNotLockedExclusively();
                else
                    TryAcquireLock(lockMode.Value);
            }
        }

        /// <summary>Generates exception if object is exclusively locked by another user or process.</summary>
        private void EnsureNotLockedExclusively()
        {
            bool isLockedExclusively = !LockUtil.CanRead(UserSession.Actor, this.BizObject.Table.TableName, this.BizObject.GetPrimaryKeyValue());
            if (isLockedExclusively)
                throw new BrokenRuleException(LockUtil.EntityIsExclusivelyLockedRuleKey, this.BizObject.MessageSource.GetString(LockUtil.EntityIsExclusivelyLockedRuleKey));
        }

        /// <summary>Tries to acquire a lock. Falls back to readonly mode if lock cannot be acquired.</summary>
        private void TryAcquireLock(LockMode lockMode)
        {
            bool lockAcquired = LockUtil.AcquireLock(UserSession.Actor, lockMode, null, this.BizObject.Table.TableName, this.BizObject.GetPrimaryKeyValue());
            if (!lockAcquired)
                FallbackToReadOnlyMode();
        }

        /// <summary>Falls back to read-only mode unless an exclusive lock is encounted in which case an exception is generated.</summary>
        private void FallbackToReadOnlyMode()
        {
            this.BizObject.IsReadOnly = true;
            bool isLockedExclusively = !LockUtil.CanRead(UserSession.Actor, this.BizObject.Table.TableName, this.BizObject.GetPrimaryKeyValue());
            if (isLockedExclusively)
                throw new BrokenRuleException(LockUtil.EntityIsExclusivelyLockedRuleKey, this.BizObject.MessageSource.GetString(LockUtil.EntityIsExclusivelyLockedRuleKey));

            JScript.Alert(this.BizObject.MessageSource.GetString(LockUtil.EntityIsLockedRuleKey));
        }

        /// <summary>Releases locks if pessimistic conccurency control is used. Does nothing if object is not persisted.
        /// Called in all event handlers raised when end business transaction is completed: save, deactivate, cancel.
        /// By default it is not automatically called in events that redirect to dedicated forms for related entities.</summary>
        protected virtual void CheckInExistingObject()
        {
            bool usesPessimisticConcurrencyControl = this.BizObject.GetDescriptor().PessimisticLockMode.HasValue;
            if (usesPessimisticConcurrencyControl && this.BizObject.GetPrimaryKeyValue() != null)
                LockUtil.ReleaseLock(UserSession.Actor, this.BizObject.Table.TableName, this.BizObject.GetPrimaryKeyValue());
        }

        #endregion

        #region Events: saved, canceled, deactivated, open child or parent form, async exceptions.

        /// <summary>Closes current form when the object is succesfully saved. Returns parameters to previous form, if any, as specified in <see cref="WebFormContext"/>. Removes object from the session using the <see cref="CleanUp"/> method. User is returned to previous/openner page.</summary>
        protected virtual void HandleSaved(object sender, EventArgs e)
        {
            CheckInExistingObject();
            this.FormContext.Close(CloseFormStatus.Success, this.BizObject, CleanUp, UiMessages.DataSuccessfullySaved);
        }

        /// <summary>Closes current form when the user canceled changes. Removes object from the session using the <see cref="CleanUp"/> method. User is returned to previous/openner page defined in <see cref="WebFormContext"/>.</summary>
        protected virtual void HandleCanceled(object sender, EventArgs e)
        {
            CheckInExistingObject();
            this.FormContext.Close(CloseFormStatus.Cancel, null, CleanUp, null);
        }

        /// <summary>Closes current form when the object is deactivated/deleted. Removes object from the session using the <see cref="CleanUp"/> method. User is returned to previous/openner page defined in <see cref="WebFormContext"/>.</summary>
        protected virtual void HandleDeactivated(object sender, EventArgs e)
        {
            CheckInExistingObject();
            this.FormContext.Close(CloseFormStatus.Cancel, null, CleanUp, UiMessages.DataSuccessfullySaved);
        }

        /// <summary>Opens a popup form used to create a new parent entity. Sets <see cref="WebFormContext"/> information for popup form so that it is able to update openner form with new parent data.</summary>
        protected virtual void HandleNewParent(object sender, EventArgs<WebFormContext> e)
        {
            e.Item.Mode = OpenFormMode.Popup;
            e.Item.Open();
        }

        /// <summary>Opens a readonly popup form that display parent entity's data. Sets <see cref="WebFormContext"/> information for popup form so that it know it has to close the popup.</summary>
        protected virtual void HandleOpenParent(object sender, EventArgs<WebFormContext> e)
        {
            e.Item.Mode = OpenFormMode.Popup;
            e.Item.ReadOnly = true;
            e.Item.Open();
        }

        /// <summary>Opens a page which is used to crete a new child entity. Sets <see cref="WebFormContext"/> information for the new page so that it is able to return the user back to the current page. Passes current (parent) entity's PK and maps it to child FK. If <see cref="OpenFormMode.Redirect"/> mode is used then the current object is removed from the session using the <see cref="CleanUp"/> method.</summary>
        protected virtual void HandleNewChild(object sender, EventArgs<WebFormContext> e)
        {
            if (e.Item.Mode == OpenFormMode.Redirect)
                CleanUp();

            e.Item.Open();
        }

        /// <summary>Opens a readonly page which is used to view a new child entity. Sets <see cref="WebFormContext"/> information for the new page so that it is able to return the user back to the current page. If <see cref="OpenFormMode.Redirect"/> mode is used then the current object is removed from the session using the <see cref="CleanUp"/> method.</summary>
        protected virtual void HandleOpenChild(object sender, EventArgs<WebFormContext> e)
        {
            if (e.Item.Mode == OpenFormMode.Redirect)
                CleanUp();

            e.Item.Open();
        }

        /// <summary>Handles exceptions which occur during the async post-backs, ie. passes them to <see cref="ExceptionController"/>.</summary>
        protected virtual void HandleAsyncPostBackError(object sender, AsyncPostBackErrorEventArgs e)
        {
            ExceptionController.RedirectToErrorForm(e.Exception);
        }

        #endregion
    }
}
